<!-- tabs标签 -->
<template>
  <view class="aster-tabs">
    <view class="aster-tabs__wrapper">
      <slot name="left"></slot>
      <view class="aster-tabs__wrapper__scroll-view-wrapper">
        <scroll-view
          :scroll-x="props.scrollable"
          :scroll-left="scrollLeft"
          scroll-with-animation
          class="aster-tabs__wrapper__scroll-view"
          :show-scrollbar="false"
          ref="aster-tabs__wrapper__scroll-view"
        >
          <view class="aster-tabs__wrapper__nav" ref="aster-tabs__wrapper__nav">
            <view
              class="aster-tabs__wrapper__nav__item"
              v-for="(item, index) in list"
              :key="index"
              @tap="clickHandler(item, index)"
              :ref="`aster-tabs__wrapper__nav__item-${index}`"
              :style="Object.assign({ flex: props.scrollable ? '' : 1 }, addStyle(props.itemStyle))"
              :class="[
                `aster-tabs__wrapper__nav__item-${index}`,
                item.disabled && 'aster-tabs__wrapper__nav__item--disabled',
              ]"
            >
              <text
                :class="[item.disabled && 'aster-tabs__wrapper__nav__item__text--disabled']"
                class="aster-tabs__wrapper__nav__item__text"
                :style="textStyle(index)"
              >
                {{ item[keyName] }}
              </text>
              <uni-badge
                v-show="!!(item.badge && (item.badge.isDot || item.badge.text))"
                :is-dot="(item.badge && item.badge.isDot) || false"
                :text="(item.badge && item.badge.text) || ' '"
                :max-num="(item.badge && item.badge.maxNum) || 99"
                :type="(item.badge && item.badge.type) || 'error'"
                :inverted="(item.badge && item.badge.inverted) || false"
                :custom-style="{ marginLeft: '4px' }"
              />
            </view>
            <view
              class="aster-tabs__wrapper__nav__line"
              ref="aster-tabs__wrapper__nav__line"
              :style="{
                width: addUnit(props.lineWidth),
                transform: `translate(${lineOffsetLeft}px)`,
                transitionDuration: `${firstTime ? 0 : duration}ms`,
                height: addUnit(lineHeight),
                background: lineColor,
                backgroundSize: lineBgSize,
              }"
            />
          </view>
        </scroll-view>
      </view>
      <slot name="right"></slot>
    </view>
  </view>
</template>
<script setup lang="ts">
  import { addStyle, deepMerge, sleep, addUnit } from '@/utils';
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect } from 'vue';

  const emits = defineEmits(['click', 'change']);
  const props = defineProps({
    // 滑块的移动过渡时间，单位ms
    duration: {
      type: Number,
      default: () => 300,
    },
    // tabs标签数组
    list: {
      type: Array<any>,
      default: () => [],
    },
    // 滑块颜色
    lineColor: {
      type: String,
      default: () => '#3c9cff',
    },
    // 菜单选择中时的样式
    activeStyle: {
      type: Object,
      default: () => ({
        color: '#303133',
      }),
    },
    // 菜单非选中时的样式
    inactiveStyle: {
      type: Object,
      default: () => ({
        color: '#606266',
      }),
    },
    // 滑块长度
    lineWidth: {
      type: Number,
      default: () => 40,
    },
    // 滑块高度
    lineHeight: {
      type: Number,
      default: () => 6,
    },
    // 滑块背景显示大小，当滑块背景设置为图片时使用
    lineBgSize: {
      type: String,
      default: () => 'cover',
    },
    // 菜单item的样式
    itemStyle: {
      type: Object,
      default: () => ({
        height: '44px',
      }),
    },
    // 菜单是否可滚动
    scrollable: {
      type: Boolean,
      default: () => true,
    },
    // 当前选中标签的索引
    current: {
      type: Number,
      default: () => 0,
    },
    // 默认读取的键名
    keyName: {
      type: String,
      default: () => 'name',
    },
  });

  const data: any = ref();
  const firstTime = ref(true);
  const scrollLeft = ref(0);
  const scrollViewWidth = ref(0);
  const lineOffsetLeft = ref(0);
  const tabsRect: any = ref({
    left: 0,
  });
  const innerCurrent = ref(0);

  const timer = ref();

  watch(
    () => props.current,
    (newValue, oldValue) => {
      // 内外部值不相等时，才尝试移动滑块
      if (newValue !== innerCurrent.value) {
        innerCurrent.value = newValue;
        nextTick(() => {
          resize();
        });
      }
    },
  );

  watchEffect(() => {
    data.value = props.list;
    nextTick(() => {
      resize();
    });
  });

  const textStyle = computed(() => {
    return (index: number) => {
      const style: any = {};
      // 取当期是否激活的样式
      const customeStyle =
        index === innerCurrent.value ? addStyle(props.activeStyle) : addStyle(props.inactiveStyle);
      // 如果当前菜单被禁用，则加上对应颜色，需要在此做处理，是因为nvue下，无法在style样式中通过!import覆盖标签的内联样式
      if (data.value[index]?.disabled) {
        style.color = '#c8c9cc';
      }
      return deepMerge(customeStyle, style);
    };
  });

  onMounted(() => {
    init();
  });

  const setLineLeft = () => {
    const tabItem = data.value[innerCurrent.value];
    if (!tabItem) {
      return;
    }
    // 获取滑块该移动的位置
    let offsetLeft = data.value
      .slice(0, innerCurrent.value)
      .reduce((total: any, curr: any) => total + curr.rect.width, 0);
    // 获取下划线的数值px表示法
    lineOffsetLeft.value = offsetLeft + (tabItem.rect.width - props.lineWidth / 2) / 2;

    // 如果是第一次执行此方法，让滑块在初始化时，瞬间滑动到第一个tab item的中间
    // 这里需要一个定时器，因为在非nvue下，是直接通过style绑定过渡时间，需要等其过渡完成后，再设置为false(非第一次移动滑块)
    if (firstTime.value) {
      timer.value = setTimeout(() => {
        firstTime.value = false;
      }, 10);
    }
  };

  // 点击某一个标签
  const clickHandler = (item: any, index: number) => {
    // 因为标签可能为disabled状态，所以click是一定会发出的，但是change事件是需要可用的状态才发出
    emits('click', {
      ...item,
      index,
    });
    // 如果disabled状态，返回
    if (item.disabled) return;
    innerCurrent.value = index;
    resize();
    emits('change', {
      ...item,
      index,
    });
  };

  const init = () => {
    sleep().then(() => {
      resize();
    });
  };

  const setScrollLeft = () => {
    // 当前活动tab的布局信息，有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
    const tabRect = data.value[innerCurrent.value];
    // 累加得到当前item到左边的距离
    const offsetLeft = data.value.slice(0, innerCurrent.value).reduce((total: any, curr: any) => {
      return total + curr.rect.width;
    }, 0);
    // 此处为屏幕宽度
    const windowWidth = uni.getSystemInfoSync().windowWidth;
    // 将活动的tabs-item移动到屏幕正中间，实际上是对scroll-view的移动
    let left =
      offsetLeft -
      (tabsRect.value.width - tabRect.rect.width) / 2 -
      (windowWidth - tabsRect.value.right) / 2 +
      tabsRect.value.left / 2;
    // 这里做一个限制，限制scrollLeft的最大值为整个scroll-view宽度减去tabs组件的宽度
    left = Math.min(left, scrollViewWidth.value - tabsRect.value.width);
    scrollLeft.value = Math.max(0, left);
  };

  // 获取所有标签的尺寸
  const resize = () => {
    // 如果不存在list，则不处理
    if (data.value.length === 0) {
      return;
    }
    Promise.all([getTabsRect(), getAllItemRect()]).then((res: any) => {
      const tabs: any = res[0];
      const itemRect: any = res[1];
      tabsRect.value = Object.assign(tabsRect, tabs);
      scrollViewWidth.value = 0;
      itemRect.map((item: any, index: number) => {
        // 计算scroll-view的宽度，这里
        scrollViewWidth.value = scrollViewWidth.value + item.width;
        // 另外计算每一个item的中心点X轴坐标
        data.value[index].rect = item;
      });
      // 获取了tabs的尺寸之后，设置滑块的位置
      setLineLeft();
      setScrollLeft();
    });
  };

  // 获取导航菜单的尺寸
  const getTabsRect = () => {
    return new Promise((resolve) => {
      queryRect('aster-tabs__wrapper__scroll-view', false).then((size) => resolve(size));
    });
  };

  // 获取所有标签的尺寸
  const getAllItemRect = () => {
    return new Promise((resolve) => {
      const promiseAllArr = data.value.map((item: any, index: number) =>
        queryRect(`aster-tabs__wrapper__nav__item-${index}`, true),
      );
      Promise.all(promiseAllArr).then((sizes) => resolve(sizes));
    });
  };

  // 获取各个标签的尺寸
  const queryRect = (el: any, item: boolean) => {
    // $uGetRect为uView自带的节点查询简化方法，详见文档介绍：https://www.uviewui.com/js/getRect.html
    // 组件内部一般用this.$uGetRect，对外的为uni.$u.getRect，二者功能一致，名称不同
    return new Promise((resolve) => {
      $uGetRect(`.${el}`, false).then((size) => {
        resolve(size);
      });
    });
  };

  const $uGetRect = (selector: any, all: any) => {
    return new Promise((resolve) => {
      uni
        .createSelectorQuery()
        .in(this)
        [all ? 'selectAll' : 'select'](selector)
        .boundingClientRect((rect) => {
          if (all && Array.isArray(rect) && rect.length) {
            resolve(rect);
          }
          if (!all && rect) {
            resolve(rect);
          }
        })
        .exec();
    });
  };

  /** 页面销毁前清除定时器 */
  onBeforeUnmount(() => {
    clearInterval(timer.value);
  });
</script>
<style lang="scss" scoped>
  view,
  scroll-view,
  swiper-item {
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    flex-grow: 0;
    flex-basis: auto;
    align-items: stretch;
    align-content: flex-start;
  }
  .aster-tabs {
    &__wrapper {
      display: flex;
      flex-direction: row;
      align-items: center;

      &__scroll-view-wrapper {
        flex: 1;
        overflow: auto hidden;
      }

      &__scroll-view {
        display: flex;
        flex-direction: row;
        flex: 1;
        white-space: nowrap;
      }

      &__nav {
        display: flex;
        flex-direction: row;
        position: relative;

        &__item {
          padding: 0 11px;
          display: flex;
          flex-direction: row;
          align-items: center;
          justify-content: center;

          &--disabled {
            cursor: not-allowed;
          }

          &__text {
            font-size: 15px;
            color: #606266;

            &--disabled {
              color: #c8c9cc !important;
            }
          }
        }

        &__line {
          height: 3px;
          background: #3c9cff;
          width: 30px;
          position: absolute;
          bottom: 2px;
          border-radius: 100px;
          transition-property: transform;
          transition-duration: 300ms;
        }
      }
    }
  }
</style>
